package net.j4love.mybatis.kit.plugin;


import net.j4love.mybatis.kit.CommonObjectCache;
import net.j4love.mybatis.kit.executor.ExecutorEnvPair;
import net.j4love.mybatis.kit.executor.ExecutorManager;
import net.j4love.mybatis.kit.sql.SQLParserUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.BaseExecutor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Set;


/**
 * 读写分离拦截器
 * @author he peng
 * @date 2018-09-05
 */

@Intercepts({
        @Signature(type = Executor.class ,
                method = "query" ,
                args = {MappedStatement.class , Object.class , RowBounds.class ,
                        ResultHandler.class , CacheKey.class , BoundSql.class})
        , @Signature(type = Executor.class ,
        method = "query" ,
        args = {MappedStatement.class , Object.class , RowBounds.class ,
                ResultHandler.class})}
)
public class ReadWriteSeparateInterceptor implements Interceptor {

    private static final Log LOG = LogFactory.getLog(ReadWriteSeparateInterceptor.class);
    private Properties props;
    private ExecutorManager executorManager;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        BoundSql boundSql = ms.getBoundSql(invocation.getArgs()[1]);
        Set<String> selectTables = SQLParserUtils.getTableNames(boundSql.getSql(), CommonObjectCache.DATABASE_TYPE_KEY);
        Set<String> updatedTables = BoundThreadUpdatedTableUtils.getAndClear();

        if (! CollectionUtils.containsAny(updatedTables , selectTables)) {
            ExecutorEnvPair executorEnvPair = this.executorManager.getRandomSlaveExecutorEnvPair();
            if (executorEnvPair != null) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("select from slave database , url -> " + executorEnvPair.getEnv().getUrl());
                }
                return invocation.getMethod().invoke(executorEnvPair.getExecutor() , invocation.getArgs());
            }
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("select from master database , url -> " + this.executorManager.getMasterExecutorEnvPair().getEnv().getUrl());
        }
        return invocation.proceed();
    }

    @Override
    public synchronized Object plugin(Object target) {
        if (target instanceof Executor) {
            String mybatisConfigPath = this.props.getProperty("mybatisConfigPath");
            if (this.executorManager == null) {
                List<Class<? extends Interceptor>> interceptors = new ArrayList<>();
                interceptors.add(this.getClass());
                this.executorManager = ExecutorManager.newExecutorManager(mybatisConfigPath, interceptors);
            }

            try {
                Configuration conf = net.j4love.mybatis.kit.executor.Executors.getConfigurationFromBaseExecutor((BaseExecutor) target);
                DatabaseMetaData metaData = conf.getEnvironment().getDataSource().getConnection().getMetaData();
                CommonObjectCache.put(CommonObjectCache.DATABASE_META_DATA_KEY, metaData);
                CommonObjectCache.put(CommonObjectCache.DATABASE_TYPE_KEY , metaData.getDatabaseProductName());
            } catch (SQLException e) {
                LOG.error("cache database metadata error." , e);
            }
            return Plugin.wrap(Plugin.wrap(target , new UpdateDetectionInterceptor()) , this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {
        this.props = properties;
    }
}
